/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen.media.rmi;

/*
XXX
See if the SerializableRenderedImage can be sent by requests instead of
deep copy.
*/

import java.awt.*;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.RenderContext;
import java.awt.image.renderable.RenderableImage;
import java.io.*;
import java.net.*;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import org.eclipse.imagen.OperationRegistry;
import org.eclipse.imagen.media.serialize.SerializableState;
import org.eclipse.imagen.media.serialize.SerializerFactory;
import org.eclipse.imagen.remote.SerializableRenderedImage;
import org.eclipse.imagen.tilecodec.TileCodecParameterList;
import org.eclipse.imagen.tilecodec.TileDecoderFactory;
import org.eclipse.imagen.tilecodec.TileEncoderFactory;
import org.eclipse.imagen.util.CaselessStringKey;

/**
 * A serializable wrapper class for classes which implement the <code>RenderableImage</code> interface.
 *
 * <p>A <code>SerializableRenderableImage</code> provides a means to serialize a <code>RenderableImage</code>. Transient
 * fields are handled using <code>Serializer</code>s registered with the <code>SerializerFactory</code>. Since no data
 * is associated with a <code>RenderableImage</code>, <code>SerializableRenderableImage</code> does not provide any
 * renderable image data. The only way to access image data from a <code>SerializableRenderableImage</code> is by
 * calling any one of the <code>createDefaultRendering</code>, <code>createRendering</code>, or <code>
 * createScaledRendering</code> methods. The resultant <code>RenderedImage</code> is created on the remote host and
 * provided via deep copy of the image data. If the request is made on the local host, the image data are provided by
 * forwarding the request to the wrapped <code>RenderableImage</code>. Note that a single <code>
 * SerializableRenderableImage</code> object should be able to service multiple remote hosts.
 *
 * <p>An example of the usage of this class is as follows:
 *
 * <pre>
 * import java.io.IOException;
 * import java.io.ObjectInputStream;
 * import java.io.ObjectOutputStream;
 * import java.io.Serializable;
 *
 * public class SomeSerializableClass implements Serializable {
 *     protected transient RenderableImage image;
 *
 *     // Fields omitted.
 *
 *     public SomeSerializableClass(RenderableImage image) {
 *         this.image = image;
 *     }
 *
 *     // Methods omitted.
 *
 *     // Serialization method.
 *     private void writeObject(ObjectOutputStream out) throws IOException {
 *         out.defaultWriteObject();
 *         out.writeObject(new SerializableRenderableImage(image));
 *     }
 *
 *     // Deserialization method.
 *     private void readObject(ObjectInputStream in)
 *         throws IOException, ClassNotFoundException {
 *         in.defaultReadObject();
 *         image = (RenderableImage)in.readObject();
 *     }
 * }
 * </pre>
 *
 * @see java.awt.image.renderable.RenderableImage
 * @see org.eclipse.imagen.RenderableOp
 */
public final class SerializableRenderableImage implements RenderableImage, Serializable {

    /** Value to indicate the server socket timeout period (milliseconds). */
    private static final int SERVER_TIMEOUT = 60000; // XXX 1 minute?

    /** Message indicating that a client will not connect again. */
    private static final String CLOSE_MESSAGE = "CLOSE";

    /** Flag indicating whether this is a data server. */
    private transient boolean isServer;

    /** The RenderableImage source of this object (server only). */
    private transient RenderableImage source;

    /** The X coordinate of the image's upper-left pixel. */
    private float minX;

    /** The Y coordinate of the image's upper-left pixel. */
    private float minY;

    /** The image's width in pixels. */
    private float width;

    /** The image's height in pixels. */
    private float height;

    /** The image's sources, stored in a Vector. */
    private transient Vector sources = null;

    /** A Hashtable containing the image properties. */
    private transient Hashtable properties = null;

    /** */
    private boolean isDynamic;

    /** The Internet Protocol (IP) address of the instantiating host. */
    private InetAddress host;

    /** The port on which the data server is listening. */
    private int port;

    /** Flag indicating that the server is available for connections. */
    private transient boolean serverOpen = false;

    /** The server socket for image data transfer (server only). */
    private transient ServerSocket serverSocket = null;

    /** The thread in which the data server is running (server only). */
    private transient Thread serverThread;

    /**
     * A table of counts of remote references to instances of this class (server only).
     *
     * <p>This table consists of entries with the keys being instances of <code>SerializableRenderableImage</code> and
     * the values being <code>Integer</code>s the int value of which represents the number of remote <code>
     * SerializableRenderableImage</code> objects which could potentially request a socket connection with the
     * associated key. This table is necessary to prevent the garbage collector of the interpreter in which the server
     * <code>SerializableRenderableImage</code> object is instantiated from finalizing the object - and thereby closing
     * its server socket - when that object could still receive socket connection requests from its remote clients. The
     * reference to the object in the static class variable ensures that the object will not be prematurely finalized.
     */
    private static transient Hashtable remoteReferenceCount;

    /** Indicate that tilecodec is used in the transfering or not */
    private boolean useTileCodec = false;

    /**
     * The <code>OperationRegistry</code> to be used to find the <code>TileEncoderFactory</code> and <code>
     * TileDecoderFactory</code>
     */
    private OperationRegistry registry = null;

    /** The name of the TileCodec format. */
    private String formatName = null;

    /** Cache the encoding/decoding parameters */
    private TileCodecParameterList encodingParam = null;

    private TileCodecParameterList decodingParam = null;

    /**
     * Increment the remote reference count of the argument.
     *
     * <p>If the argument is not already in the remote reference table, add it to the table with a count value of unity.
     * If it exists in table, increment its count value.
     *
     * @parameter o The object the count value of which is to be incremented.
     */
    private static synchronized void incrementRemoteReferenceCount(Object o) {
        if (remoteReferenceCount == null) {
            remoteReferenceCount = new Hashtable();
            remoteReferenceCount.put(o, new Integer(1));
        } else {
            Integer count = (Integer) remoteReferenceCount.get(o);
            if (count == null) {
                remoteReferenceCount.put(o, new Integer(1));
            } else {
                remoteReferenceCount.put(o, new Integer(count.intValue() + 1));
            }
        }
    }

    /**
     * Decrement the remote reference count of the argument.
     *
     * <p>If the count value of the argument exists in the table its count value is decremented unless the count value
     * is unity in which case the entry is removed from the table.
     *
     * @parameter o The object the count value of which is to be decremented.
     */
    private static synchronized void decrementRemoteReferenceCount(Object o) {
        if (remoteReferenceCount != null) {
            Integer count = (Integer) remoteReferenceCount.get(o);
            if (count != null) {
                if (count.intValue() == 1) {
                    remoteReferenceCount.remove(o);
                } else {
                    remoteReferenceCount.put(o, new Integer(count.intValue() - 1));
                }
            }
        }
    }

    /** The default constructor. */
    SerializableRenderableImage() {}

    /**
     * Constructs a <code>SerializableRenderableImage</code> wrapper for a <code>RenderableImage</code> source. The
     * image data of the rendering will be serialized via a single deep copy. Tile encoding and decoding may be effected
     * via a <code>TileEncoder</code> and <code>TileDecoder</code> specified by format name.
     *
     * @param source The <code>RenderableImage</code> source.
     * @param registry The <code>OperationRegistry</code> to use in creating the <code>TileEncoder</code>. The <code>
     *     TileDecoder</code> will of necessity be created using the default <code>OperationRegistry</code> as the
     *     specified <code>OperationRegistry</code> is not serialized. If <code>null</code> the default registry will be
     *     used.
     * @param formatName The name of the format used to encode the data. If <code>null</code> simple tile serialization
     *     will be performed either directly or by use of a "raw" <code>TileCodec</code>.
     * @param encodingParam The parameters to be used for data encoding. If <code>null</code> the default encoding
     *     <code>TileCodecParameterList</code> for this format will be used. Ignored if <code>formatName</code> is
     *     <code>null</code>.
     * @param decodingParam The parameters to be used for data decoding. If <code>null</code> a complementary <code>
     *     TileCodecParameterList</code> will be derived from <code>encodingParam</code>. Ignored if <code>formatName
     *     </code> is <code>null</code>.
     * @exception IllegalArgumentException if <code>source</code> or <code>formatName</code> is <code>null</code>.
     * @exception IllegalArgumentException if <code>encodingParam</code> and <code>decodingParam</code> do not have the
     *     same format name as the supplied <code>formatName</code>.
     */
    public SerializableRenderableImage(
            RenderableImage source,
            OperationRegistry registry,
            String formatName,
            TileCodecParameterList encodingParam,
            TileCodecParameterList decodingParam) {

        this(source);

        this.registry = registry;
        this.formatName = formatName;
        this.encodingParam = encodingParam;
        this.decodingParam = decodingParam;

        if (formatName == null) {
            throw new IllegalArgumentException(JaiI18N.getString("SerializableRenderableImage2"));
        }

        if (!formatName.equals(encodingParam.getFormatName())) {
            throw new IllegalArgumentException(JaiI18N.getString("UseTileCodec0"));
        }

        if (!formatName.equals(decodingParam.getFormatName())) {
            throw new IllegalArgumentException(JaiI18N.getString("UseTileCodec1"));
        }

        TileEncoderFactory tileEncoderFactory = (TileEncoderFactory) registry.getFactory("tileEncoder", formatName);
        TileDecoderFactory tileDecoderFactory = (TileDecoderFactory) registry.getFactory("tileDecoder", formatName);
        if (tileEncoderFactory == null || tileDecoderFactory == null)
            throw new RuntimeException(JaiI18N.getString("UseTileCodec2"));

        useTileCodec = true;
    }

    /**
     * Constructs a <code>SerializableRenderableImage</code> wrapper for a <code>RenderableImage</code> source. Image
     * data of the rendering of the <code>RenderableImage</code> will be serialized via a single deep copy. No <code>
     * TileCodec</code> will be used, i.e., data will be transmitted using the serialization protocol for <code>Raster
     * </code>s.
     *
     * @param source The <code>RenderableImage</code> source.
     * @exception IllegalArgumentException if <code>source</code> or <code>formatName</code> is <code>null</code>.
     */
    public SerializableRenderableImage(RenderableImage source) {

        if (source == null) throw new IllegalArgumentException(JaiI18N.getString("SerializableRenderableImage1"));

        // Set server flag.
        isServer = true;

        // Cache the parameter.
        this.source = source;

        // Initialize RenderableImage fields.
        minX = source.getMinX();
        minY = source.getMinY();
        width = source.getWidth();
        height = source.getHeight();
        isDynamic = source.isDynamic();

        sources = new Vector();
        sources.add(source);

        properties = new Hashtable();
        String[] propertyNames = source.getPropertyNames();
        String propertyName;
        if (propertyNames != null) {
            for (int i = 0; i < propertyNames.length; i++) {
                propertyName = propertyNames[i];
                properties.put(new CaselessStringKey(propertyName), source.getProperty(propertyName));
            }
        }

        // Initialize the host field.
        try {
            host = InetAddress.getLocalHost();
        } catch (UnknownHostException e) {
            throw new RuntimeException(e.getMessage());
        }

        // Unset the server availability flag.
        serverOpen = false;
    }

    /** Private implementation of tile server. */
    private class RenderingServer implements Runnable {

        /**
         * Provide Rasters to clients on request.
         *
         * <p>This method is called by the data server thread when a deep copy of the source image Raster is not being
         * used. A socket connection is set up at a well known address to which clients may connect. After a client
         * connects it transmits a Rectangle object which is read by this method. The Raster corresponding to this
         * Rectangle is then retrieved from the source image and transmitted back over the socket connection.
         *
         * <p>The server loop will continue until this object is garbage collected.
         */
        public void run() {
            // Loop while the server availability flag is set.
            while (serverOpen) {
                // Wait for a client connection request.
                Socket socket = null;
                try {
                    socket = serverSocket.accept();
                    socket.setSoLinger(true, 1);
                } catch (InterruptedIOException e) {
                    // accept() timeout: restart loop to check
                    // availability flag.
                    continue;
                } catch (Exception e) {
                    throw new RuntimeException(e.getMessage());
                }

                // Get the socket input and output streams and wrap object
                // input and output streams around them, respectively.
                InputStream in = null;
                OutputStream out = null;
                ObjectInputStream objectIn = null;
                ObjectOutputStream objectOut = null;
                try {
                    in = socket.getInputStream();
                    out = socket.getOutputStream();
                    objectIn = new ObjectInputStream(in);
                    objectOut = new ObjectOutputStream(out);
                } catch (Exception e) {
                    throw new RuntimeException(e.getMessage());
                }

                // Read the Object from the object stream.
                Object obj = null;
                try {
                    obj = objectIn.readObject();
                } catch (Exception e) {
                    throw new RuntimeException(e.getMessage());
                }

                RenderedImage ri = null;
                SerializableRenderedImage sri;
                // Switch according to object class; ignore unsupported types.
                if (obj instanceof String) {
                    String str = (String) obj;

                    if (str.equals(CLOSE_MESSAGE)) {
                        // Decrement the remote reference count.
                        decrementRemoteReferenceCount(this);

                    } else {
                        if (str.equals("createDefaultRendering")) {

                            ri = source.createDefaultRendering();

                        } else if (str.equals("createRendering")) {

                            // Read the Object from the object stream.
                            obj = null;
                            try {
                                obj = objectIn.readObject();
                            } catch (Exception e) {
                                throw new RuntimeException(e.getMessage());
                            }

                            SerializableState ss = (SerializableState) obj;
                            RenderContext rc = (RenderContext) ss.getObject();

                            ri = source.createRendering(rc);

                        } else if (str.equals("createScaledRendering")) {

                            // Read the Object from the object stream.
                            obj = null;
                            try {
                                obj = objectIn.readObject();
                            } catch (Exception e) {
                                throw new RuntimeException(e.getMessage());
                            }

                            int w = ((Integer) obj).intValue();

                            try {
                                obj = objectIn.readObject();
                            } catch (Exception e) {
                                throw new RuntimeException(e.getMessage());
                            }

                            int h = ((Integer) obj).intValue();

                            try {
                                obj = objectIn.readObject();
                            } catch (Exception e) {
                                throw new RuntimeException(e.getMessage());
                            }

                            SerializableState ss = (SerializableState) obj;
                            RenderingHints rh = (RenderingHints) ss.getObject();

                            ri = source.createScaledRendering(w, h, rh);
                        }

                        if (useTileCodec) {
                            try {
                                sri = new SerializableRenderedImage(
                                        ri, true, registry, formatName, encodingParam, decodingParam);
                            } catch (java.io.NotSerializableException nse) {
                                throw new RuntimeException(nse.getMessage());
                            }
                        } else {
                            sri = new SerializableRenderedImage(ri, true);
                        }

                        try {
                            objectOut.writeObject(sri);
                        } catch (Exception e) {
                            throw new RuntimeException(e.getMessage());
                        }
                    }
                } else {
                    throw new RuntimeException(JaiI18N.getString("SerializableRenderableImage0"));
                }

                // XXX Concerning serialization of properties, perhaps the
                // best approach would be to serialize all the properties up
                // front if a deep copy were being made but otherwise to wait
                // until the first property request was received before
                // transmitting any property values. When the first request
                // was made, all property values would be transmitted and then
                // cached. Up front serialization might in both cases include
                // transmitting all names. If property serialization were
                // deferred, then a new message branch would be added here
                // to retrieve the properties which could be obtained as
                // a PropertySourceImpl. If properties are also served up
                // then this inner class should be renamed "DataServer".

                // Close the various streams and the socket itself.
                try {
                    objectOut.close();
                    objectIn.close();
                    out.close();
                    in.close();
                    socket.close();
                } catch (Exception e) {
                    throw new RuntimeException(e.getMessage());
                }
            }
        }
    }

    // --- Begin implementation of java.awt.image.RenderableImage. ---

    /**
     * Returns a <code>RenderedImage</code> which is the result of calling <code>createDefaultRendering</code> on the
     * wrapped <code>RenderableImage</code>.
     */
    public RenderedImage createDefaultRendering() {

        if (isServer) {
            return source.createDefaultRendering();
        }

        // Connect to the data server.
        Socket socket = connectToServer();

        // Get the socket input and output streams and wrap object
        // input and output streams around them, respectively.
        OutputStream out = null;
        ObjectOutputStream objectOut = null;
        InputStream in = null;
        ObjectInputStream objectIn = null;
        try {
            out = socket.getOutputStream();
            objectOut = new ObjectOutputStream(out);
            in = socket.getInputStream();
            objectIn = new ObjectInputStream(in);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }

        // Write the name of the method to the object output stream.
        try {
            objectOut.writeObject("createDefaultRendering");
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }

        // Read serialized form of the RenderedImage from object output stream.
        Object object = null;
        try {
            object = objectIn.readObject();
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }

        RenderedImage ri;
        if (object instanceof SerializableRenderedImage) {
            ri = (RenderedImage) object;
        } else {
            ri = null;
        }

        // Close the various streams and the socket.
        try {
            out.close();
            objectOut.close();
            in.close();
            objectIn.close();
            socket.close();
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }

        return ri;
    }

    public RenderedImage createRendering(RenderContext renderContext) {

        if (isServer) {
            return source.createRendering(renderContext);
        }

        // Connect to the data server.
        Socket socket = connectToServer();

        // Get the socket input and output streams and wrap object
        // input and output streams around them, respectively.
        OutputStream out = null;
        ObjectOutputStream objectOut = null;
        InputStream in = null;
        ObjectInputStream objectIn = null;
        try {
            out = socket.getOutputStream();
            objectOut = new ObjectOutputStream(out);
            in = socket.getInputStream();
            objectIn = new ObjectInputStream(in);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }

        // Write the name of the method and the RenderContext to the
        // object output stream.
        try {
            objectOut.writeObject("createRendering");
            objectOut.writeObject(SerializerFactory.getState(renderContext, null));
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }

        // Read serialized form of the RenderedImage from object output stream.
        Object object = null;
        try {
            object = objectIn.readObject();
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }

        RenderedImage ri = (RenderedImage) object;

        // Close the various streams and the socket.
        try {
            out.close();
            objectOut.close();
            in.close();
            objectIn.close();
            socket.close();
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }

        return ri;
    }

    public RenderedImage createScaledRendering(int w, int h, RenderingHints hints) {

        if (isServer) {
            return source.createScaledRendering(w, h, hints);
        }

        // Connect to the data server.
        Socket socket = connectToServer();

        // Get the socket input and output streams and wrap object
        // input and output streams around them, respectively.
        OutputStream out = null;
        ObjectOutputStream objectOut = null;
        InputStream in = null;
        ObjectInputStream objectIn = null;
        try {
            out = socket.getOutputStream();
            objectOut = new ObjectOutputStream(out);
            in = socket.getInputStream();
            objectIn = new ObjectInputStream(in);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }

        // Write the name of the method and the necessary method argument
        // to the object output stream.
        try {
            objectOut.writeObject("createScaledRendering");
            objectOut.writeObject(new Integer(w));
            objectOut.writeObject(new Integer(h));
            objectOut.writeObject(SerializerFactory.getState(hints, null));
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }

        // Read serialized form of the RenderedImage from object output stream.
        Object object = null;
        try {
            object = objectIn.readObject();
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }

        RenderedImage ri = (RenderedImage) object;

        // Close the various streams and the socket.
        try {
            out.close();
            objectOut.close();
            in.close();
            objectIn.close();
            socket.close();
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }

        return ri;
    }

    public float getHeight() {
        return height;
    }

    public float getMinX() {
        return minX;
    }

    public float getMinY() {
        return minY;
    }

    // XXX Should getProperty() request property values over a socket
    // connection also?
    public Object getProperty(String name) {
        Object property = properties.get(new CaselessStringKey(name));
        return property == null ? Image.UndefinedProperty : property;
    }

    public String[] getPropertyNames() {
        String[] names = null;
        if (!properties.isEmpty()) {
            names = new String[properties.size()];
            Enumeration keys = properties.keys();
            int index = 0;
            CaselessStringKey key;
            while (keys.hasMoreElements()) {
                key = (CaselessStringKey) keys.nextElement();
                names[index++] = key.getName();
            }
        }
        return names;
    }

    /**
     * If this <code>SerializableRenderableImage</code> has not been serialized, this method returns a <code>Vector
     * </code> containing only the <code>RenderableImage</code> passed to the constructor; if this image has been
     * deserialized, it returns <code>null</code>.
     */
    public Vector getSources() {
        return sources;
    }

    /** */
    public boolean isDynamic() {
        return isDynamic;
    }

    public float getWidth() {
        return width;
    }

    // --- End implementation of java.awt.image.RenderableImage. ---

    /**
     * Create a server socket and start the server in a separate thread.
     *
     * <p>Note that this method should be called only the first time this object is serialized and only if a deep copy
     * is not being used. If a deep copy is used there is no need to serve clients data on demand. However if data
     * service is being provided, there is no need to create multiple threads for the single object as a single server
     * thread should be able to service multiple remote objects.
     */
    private synchronized void openServer() throws IOException, SocketException {
        if (!serverOpen) {
            // Create a ServerSocket.
            serverSocket = new ServerSocket(0);

            // Set the ServerSocket accept() method timeout period.
            serverSocket.setSoTimeout(SERVER_TIMEOUT);

            // Initialize the port field.
            port = serverSocket.getLocalPort();

            // Set the server availability flag.
            serverOpen = true;

            // Spawn a child thread and return the parent thread to the caller.
            serverThread = new Thread(new RenderingServer());
            serverThread.start();

            // Increment the remote reference count.
            incrementRemoteReferenceCount(this);
        }
    }

    /** Transmit a message to the data server to indicate that the client will no longer request socket connections. */
    private void closeClient() {
        // Connect to the data server.
        Socket socket = connectToServer();

        // Get the socket output stream and wrap an object
        // output stream around it.
        OutputStream out = null;
        ObjectOutputStream objectOut = null;
        try {
            out = socket.getOutputStream();
            objectOut = new ObjectOutputStream(out);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }

        // Write CLOSE_MESSAGE to the object output stream.
        try {
            objectOut.writeObject(CLOSE_MESSAGE);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }

        // Close the streams and the socket.
        try {
            out.close();
            objectOut.close();
            socket.close();
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * Obtain a connection to the data server socket. This is used only if a deep copy of the image Raster has not been
     * made.
     */
    private Socket connectToServer() {
        // Open a connection to the data server.
        Socket socket = null;
        try {
            socket = new Socket(host, port);
            socket.setSoLinger(true, 1);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }

        return socket;
    }

    /**
     * Provides a hint that an image will no longer be accessed from a reference in user space. The results are
     * equivalent to those that occur when the program loses its last reference to this image, the garbage collector
     * discovers this, and finalize is called. This can be used as a hint in situations where waiting for garbage
     * collection would be overly conservative, e.g., there are a large number of socket connections which may be opened
     * to transmit tile data.
     *
     * <p><code>SerializableRenderableImage</code> defines this method to behave as follows:
     *
     * <ul>
     *   <li>if the image is acting as a server, i.e., has never been serialized and may be providing data to serialized
     *       versions of itself, it makes itself unavailable to further client requests and closes its socket;
     *   <li>if the image is acting as a client, i.e., has been serialized and may be requesting data from a remote,
     *       pre-serialization version of itself, it sends a message to its remote self indicating that it will no
     *       longer be making requests.
     * </ul>
     *
     * <p>The results of referencing an image after a call to <code>dispose()</code> are undefined.
     */
    public void dispose() {
        // Rejoin the server thread if using a socket-based server.
        if (isServer) {
            if (serverOpen) {
                // Unset availability flag so server loop exits.
                serverOpen = false;

                // Wait for the server (child) thread to die.
                try {
                    serverThread.join(2 * SERVER_TIMEOUT);
                } catch (Exception e) {
                    // Ignore the Exception.
                }

                // Close the server socket.
                try {
                    serverSocket.close();
                } catch (Exception e) {
                    // Ignore the Exception.
                }
            }
        } else { // client
            // Transmit a message to the server to indicate the child's exit.
            closeClient();
        }
    }

    /**
     * Custom serialization method. In addition to all non-transient fields, the SampleModel, source vector, and
     * properties table are serialized. If a deep copy of the source image Raster is being used this is also serialized.
     */
    private void writeObject(ObjectOutputStream out) throws IOException {

        // Start the data server.
        try {
            openServer();
        } catch (Exception e1) {
            if (e1 instanceof SocketException) { // setSoTimeout() failed.
                if (serverSocket != null) { // XXX Facultative
                    try {
                        serverSocket.close();
                    } catch (IOException e2) {
                        // Ignore the exception.
                    }
                }
            }

            // Since server socket creation failed, use a deep copy.
            serverOpen = false; // XXX Facultative
        }

        // Write non-static and non-transient fields.
        out.defaultWriteObject();

        // Remove non-serializable elements from table of properties.
        Hashtable propertyTable = properties;
        boolean propertiesCloned = false;
        Enumeration keys = propertyTable.keys();
        while (keys.hasMoreElements()) {
            Object key = keys.nextElement();
            if (!(properties.get(key) instanceof Serializable)) {
                if (!propertiesCloned) {
                    propertyTable = (Hashtable) properties.clone();
                    propertiesCloned = true;
                }
                propertyTable.remove(key);
            }
        }

        // Write the properties table.
        out.writeObject(propertyTable);
    }

    /**
     * Custom deserialization method. In addition to all non-transient fields, the SampleModel, source vector, and
     * properties table are deserialized. If a deep copy of the source image Raster is being used this is also
     * deserialized.
     */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        isServer = false;
        source = null;
        serverOpen = false;
        serverSocket = null;
        serverThread = null;

        // Read non-static and non-transient fields.
        in.defaultReadObject();

        // Read the properties table.
        properties = (Hashtable) in.readObject();
    }
}
